import axios from 'axios';
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';
import type { IRequestOption } from './interfaces';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import Cookies from 'js-cookie';
import { responseRefreshDTO } from './interfaces';
import { AxiosResponse } from 'axios/index';

/**
 * Класс для работы с вызовом API.
 * Содержит методы для отправки запросов и обработки ошибок.
 */
export class APIClient {
  api: AxiosInstance;
  errorCb = new Map<string, (error: AxiosError) => void>();
  private languageHeader = this.getLanguageSetting();
  private cookie = Cookies;
  private DeviceUUID = localStorage.getItem('D_UUID');
  private logoutCallback: () => void = () => {
    return;
  };

  get token() {
    return this.cookie.get('token');
  }

  get refreshToken() {
    return this.cookie.get('refreshToken');
  }

  constructor(
    private baseURL: string,
    private apiKey: string,
    private refreshTokenUrl: string,
  ) {
    this.api = axios.create({
      baseURL: this.baseURL,
      timeout: 30000,
      withCredentials: false,
      headers: {
        'x-api-key': this.apiKey,
      },
    });

    this.startAuthInterceptors();
  }

  /**
   * Добавляет функцию обратного вызова для обработки ошибок
   *
   * @param {string} id - ID статуса ошибки
   * @param {(error: AxiosError) => void} cb - Функция обратного вызова.
   * @return {void} Эта функция ничего не возвращает.
   */
  public addErrorCb(id: string, cb: (error: AxiosError) => void): void {
    this.errorCb.set(id, cb);
  }

  /**
   * Генерирует уникальный идентификатор устройства
   *
   * @return {Promise<string>} Уникальный идентификатор
   */
  public async genearateDeviceId(): Promise<string> {
    const fp = await FingerprintJS.load();
    const result = await fp.get();
    return result.visitorId;
  }

  /**
   * Отправляет запрос к серверу и возвращает ответ.
   * Метод принимает дженерик с типами Req и Resp, где Req - тип запроса и Resp - тип ответа.
   *
   * Если произошла ошибка, то вызывает функцию обратного вызова errorCb
   * относительно статуса ошибки.
   *
   * @param {IRequestOption<Req>} option - Опции запроса
   * @returns {Promise<Resp>} Ответ от сервера
   */
  public async request<Req, Resp>(option: IRequestOption<Req>): Promise<Resp> {
    if (option.widthCredentials === undefined) option.widthCredentials = true;

    const requestConfig: AxiosRequestConfig<Req> = {
      responseType: 'json',
      method: option.method,
      url: option.route,
      headers: {
        'Device-Id': this.DeviceUUID,
        'x-custom-lang': this.languageHeader,
        ...option.headers,
      },
    };
    if (requestConfig.headers && this.token && option.widthCredentials) {
      requestConfig.headers['Authorization'] = `Bearer ${this.token}`;
    }
    if (option.requestObj) {
      requestConfig.data = option.requestObj;
    }

    try {
      const response = await this.api.request<Resp>(requestConfig);

      return response.data;
    } catch (error: unknown) {
      if (
        axios.isAxiosError(error) &&
        error.response &&
        error.response.status &&
        this.errorCb.has(String(error.response.status))
      ) {
        const cb = this.errorCb.get(String(error.response.status));
        if (cb) {
          cb(error as AxiosError);
        }
      }

      return Promise.reject<Resp>(error);
    }
  }

  setLanguage(language: string) {
    this.languageHeader = language;
  }

  /**
   * Устанавливает токены в куки и в локальное хранилище класса
   * @param token
   * @param refreshToken
   * @param expires - Время протухания токена
   * @param remember - Установить expires для refresh token
   */
  setAuthTokens(
    token: string,
    refreshToken: string,
    expires: number,
    remember: boolean,
  ) {
    this.cookie.set('token', token, {
      expires: expires ? expires / 60 / 60 / 24 : undefined,
      secure: true,
      path: '/',
    });
    this.cookie.set('refreshToken', refreshToken, {
      path: '/',
      expires: remember ? 182 : undefined,
      secure: true,
    });
  }

  setLogoutCallback(callback: () => void) {
    this.logoutCallback = callback;
  }

  removeCookies() {
    this.cookie.remove('token');
    this.cookie.remove('refreshToken');
  }

  private getLanguageSetting(): string {
    return (localStorage.getItem('i18nextLng') as string) || 'en';
  }

  private startAuthInterceptors() {
    // Add a response interceptor to refresh the JWT token if it's expired
    this.api.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;

        // If the error is a 5000 and we have a refresh token, refresh the JWT token
        if (error.response.data.statusCode === 5000 && this.refreshToken) {
          try {
            const response = await this.api.post<
              null,
              AxiosResponse<responseRefreshDTO>
            >(this.refreshTokenUrl, null, {
              headers: {
                Authorization: `Bearer ${this.refreshToken}`,
              },
            });
            this.setAuthTokens(
              response.data.data.accessToken,
              response.data.data.refreshToken,
              response.data.data.expiresIn,
              true,
            );
            originalRequest.headers.Authorization = `Bearer ${this.token}`;
            return this.api(originalRequest);
          } catch (e) {
            this.logoutCallback();
          }
        }
        this.logoutCallback();
        // Return the original error if we can't handle it
        return Promise.reject(error);
      },
    );
  }
}
